//! Defines sys calls to interact with SpacetimeDB.
//! This forms an ABI of sorts that modules written in Rust can use.

extern crate alloc;

use core::fmt;
use core::mem::MaybeUninit;
use core::num::NonZeroU16;
use std::ptr;

use spacetimedb_primitives::{errno, errnos, ColId, IndexId, TableId};

/// Provides a raw set of sys calls which abstractions can be built atop of.
pub mod raw {
    use spacetimedb_primitives::{ColId, IndexId, TableId};

    // this module identifier determines the abi version that modules built with this crate depend
    // on. Any non-breaking additions to the abi surface should be put in a new `extern {}` block
    // with a module identifier with a minor version 1 above the previous highest minor version.
    // For breaking changes, all functions should be moved into one new `spacetime_X.0` block.
    #[link(wasm_import_module = "spacetime_10.0")]
    extern "C" {
        /// Queries the `table_id` associated with the given (table) `name`
        /// where `name` is the UTF-8 slice in WASM memory at `name_ptr[..name_len]`.
        ///
        /// The table id is written into the `out` pointer.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `name_ptr` is NULL or `name` is not in bounds of WASM memory.
        /// - `name` is not valid UTF-8.
        /// - `out` is NULL or `out[..size_of::<TableId>()]` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_TABLE`, when `name` is not the name of a table.
        pub fn table_id_from_name(name: *const u8, name_len: usize, out: *mut TableId) -> u16;

        /// Queries the `index_id` associated with the given (index) `name`
        /// where `name` is the UTF-8 slice in WASM memory at `name_ptr[..name_len]`.
        ///
        /// If a `name` was provided for the `RawIndexDef` for this index, that is the name of the index.
        /// If no name was provided, a name was autogenerated like so:
        /// ```
        /// let table_name: String = "my_table".into();
        /// let column_names: Vec<String> = vec!["bananas".into(), "oranges".into()];
        /// let column_names = column_names.join("_");
        /// let name = format!("{table_name}_{column_names}_idx_btree");
        /// ```
        /// (See the function `spacetimedb_schema::def::validate::v9::generate_index_name` for more
        /// information.)
        ///
        /// The index id is written into the `out` pointer.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `name_ptr` is NULL or `name` is not in bounds of WASM memory.
        /// - `name` is not valid UTF-8.
        /// - `out` is NULL or `out[..size_of::<IndexId>()]` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_INDEX`, when `name` is not the name of an index.
        pub fn index_id_from_name(name_ptr: *const u8, name_len: usize, out: *mut IndexId) -> u16;

        /// Writes the number of rows currently in table identified by `table_id` to `out`.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `out` is NULL or `out[..size_of::<u64>()]` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table.
        pub fn datastore_table_row_count(table_id: TableId, out: *mut u64) -> u16;

        /// Starts iteration on each row, as BSATN-encoded, of a table identified by `table_id`.
        ///
        /// On success, the iterator handle is written to the `out` pointer.
        /// This handle can be advanced by [`row_iter_bsatn_advance`].
        ///
        /// # Traps
        ///
        /// This function does not trap.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table.
        pub fn datastore_table_scan_bsatn(table_id: TableId, out: *mut RowIter) -> u16;

        /// Finds all rows in the index identified by `index_id`,
        /// according to the:
        /// - `prefix = prefix_ptr[..prefix_len]`,
        /// - `rstart = rstart_ptr[..rstart_len]`,
        /// - `rend = rend_ptr[..rend_len]`,
        ///
        /// in WASM memory.
        ///
        /// The index itself has a schema/type.
        /// The `prefix` is decoded to the initial `prefix_elems` `AlgebraicType`s
        /// whereas `rstart` and `rend` are decoded to the `prefix_elems + 1` `AlgebraicType`
        /// where the `AlgebraicValue`s are wrapped in `Bound`.
        /// That is, `rstart, rend` are BSATN-encoded `Bound<AlgebraicValue>`s.
        ///
        /// Matching is then defined by equating `prefix`
        /// to the initial `prefix_elems` columns of the index
        /// and then imposing `rstart` as the starting bound
        /// and `rend` as the ending bound on the `prefix_elems + 1` column of the index.
        /// Remaining columns of the index are then unbounded.
        /// Note that the `prefix` in this case can be empty (`prefix_elems = 0`),
        /// in which case this becomes a ranged index scan on a single-col index
        /// or even a full table scan if `rstart` and `rend` are both unbounded.
        ///
        /// The relevant table for the index is found implicitly via the `index_id`,
        /// which is unique for the module.
        ///
        /// On success, the iterator handle is written to the `out` pointer.
        /// This handle can be advanced by [`row_iter_bsatn_advance`].
        ///
        /// # Non-obvious queries
        ///
        /// For an index on columns `[a, b, c]`:
        ///
        /// - `a = x, b = y` is encoded as a prefix `[x, y]`
        ///   and a range `Range::Unbounded`,
        ///   or as a  prefix `[x]` and a range `rstart = rend = Range::Inclusive(y)`.
        /// - `a = x, b = y, c = z` is encoded as a prefix `[x, y]`
        ///   and a  range `rstart = rend = Range::Inclusive(z)`.
        /// - A sorted full scan is encoded as an empty prefix
        ///   and a range `Range::Unbounded`.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `prefix_elems > 0`
        ///   and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory).
        /// - `rstart` is NULL or `rstart` is not in bounds of WASM memory.
        /// - `rend` is NULL or `rend` is not in bounds of WASM memory.
        /// - `out` is NULL or `out[..size_of::<RowIter>()]` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index.
        /// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index.
        /// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to
        ///   a `prefix_elems` number of `AlgebraicValue`
        ///   typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type.
        ///   Or when `rstart` or `rend` cannot be decoded to an `Bound<AlgebraicValue>`
        ///   where the inner `AlgebraicValue`s are
        ///   typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type.
        pub fn datastore_index_scan_range_bsatn(
            index_id: IndexId,
            prefix_ptr: *const u8,
            prefix_len: usize,
            prefix_elems: ColId,
            rstart_ptr: *const u8, // Bound<AlgebraicValue>
            rstart_len: usize,
            rend_ptr: *const u8, // Bound<AlgebraicValue>
            rend_len: usize,
            out: *mut RowIter,
        ) -> u16;

        /// This is the same as [`datastore_index_scan_range_bsatn`].
        #[deprecated = "use `datastore_index_scan_range_bsatn` instead"]
        #[doc(alias = "datastore_index_scan_range_bsatn")]
        pub fn datastore_btree_scan_bsatn(
            index_id: IndexId,
            prefix_ptr: *const u8,
            prefix_len: usize,
            prefix_elems: ColId,
            rstart_ptr: *const u8, // Bound<AlgebraicValue>
            rstart_len: usize,
            rend_ptr: *const u8, // Bound<AlgebraicValue>
            rend_len: usize,
            out: *mut RowIter,
        ) -> u16;

        /// Deletes all rows found in the index identified by `index_id`,
        /// according to the:
        /// - `prefix = prefix_ptr[..prefix_len]`,
        /// - `rstart = rstart_ptr[..rstart_len]`,
        /// - `rend = rend_ptr[..rend_len]`,
        ///
        /// in WASM memory.
        ///
        /// This syscall will delete all the rows found by
        /// [`datastore_index_scan_range_bsatn`] with the same arguments passed,
        /// including `prefix_elems`.
        /// See `datastore_index_scan_range_bsatn` for details.
        ///
        /// The number of rows deleted is written to the WASM pointer `out`.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `prefix_elems > 0`
        ///   and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory).
        /// - `rstart` is NULL or `rstart` is not in bounds of WASM memory.
        /// - `rend` is NULL or `rend` is not in bounds of WASM memory.
        /// - `out` is NULL or `out[..size_of::<u32>()]` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index.
        /// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index.
        /// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to
        ///   a `prefix_elems` number of `AlgebraicValue`
        ///   typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type.
        ///   Or when `rstart` or `rend` cannot be decoded to an `Bound<AlgebraicValue>`
        ///   where the inner `AlgebraicValue`s are
        ///   typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type.
        pub fn datastore_delete_by_index_scan_range_bsatn(
            index_id: IndexId,
            prefix_ptr: *const u8,
            prefix_len: usize,
            prefix_elems: ColId,
            rstart_ptr: *const u8, // Bound<AlgebraicValue>
            rstart_len: usize,
            rend_ptr: *const u8, // Bound<AlgebraicValue>
            rend_len: usize,
            out: *mut u32,
        ) -> u16;

        /// This is the same as [`datastore_delete_by_index_scan_range_bsatn`].
        #[deprecated = "use `datastore_delete_by_index_scan_range_bsatn` instead"]
        #[doc(alias = "datastore_delete_by_index_scan_range_bsatn")]
        pub fn datastore_delete_by_btree_scan_bsatn(
            index_id: IndexId,
            prefix_ptr: *const u8,
            prefix_len: usize,
            prefix_elems: ColId,
            rstart_ptr: *const u8, // Bound<AlgebraicValue>
            rstart_len: usize,
            rend_ptr: *const u8, // Bound<AlgebraicValue>
            rend_len: usize,
            out: *mut u32,
        ) -> u16;

        /// Deletes those rows, in the table identified by `table_id`,
        /// that match any row in the byte string `rel = rel_ptr[..rel_len]` in WASM memory.
        ///
        /// Matching is defined by first BSATN-decoding
        /// the byte string pointed to at by `relation` to a `Vec<ProductValue>`
        /// according to the row schema of the table
        /// and then using `Ord for AlgebraicValue`.
        /// A match happens when `Ordering::Equal` is returned from `fn cmp`.
        /// This occurs exactly when the row's BSATN-encoding is equal to the encoding of the `ProductValue`.
        ///
        /// The number of rows deleted is written to the WASM pointer `out`.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `rel_ptr` is NULL or `rel` is not in bounds of WASM memory.
        /// - `out` is NULL or `out[..size_of::<u32>()]` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table.
        /// - `BSATN_DECODE_ERROR`, when `rel` cannot be decoded to `Vec<ProductValue>`
        ///   where each `ProductValue` is typed at the `ProductType` the table's schema specifies.
        pub fn datastore_delete_all_by_eq_bsatn(
            table_id: TableId,
            rel_ptr: *const u8,
            rel_len: usize,
            out: *mut u32,
        ) -> u16;

        /// Reads rows from the given iterator registered under `iter`.
        ///
        /// Takes rows from the iterator
        /// and stores them in the memory pointed to by `buffer = buffer_ptr[..buffer_len]`,
        /// encoded in BSATN format.
        ///
        /// The `buffer_len = buffer_len_ptr[..size_of::<usize>()]` stores the capacity of `buffer`.
        /// On success (`0` or `-1` is returned),
        /// `buffer_len` is set to the combined length of the encoded rows.
        /// When `-1` is returned, the iterator has been exhausted
        /// and there are no more rows to read,
        /// leading to the iterator being immediately destroyed.
        /// Note that the host is free to reuse allocations in a pool,
        /// destroying the handle logically does not entail that memory is necessarily reclaimed.
        ///
        /// # Traps
        ///
        /// Traps if:
        ///
        /// - `buffer_len_ptr` is NULL or `buffer_len` is not in bounds of WASM memory.
        /// - `buffer_ptr` is NULL or `buffer` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NO_SUCH_ITER`, when `iter` is not a valid iterator.
        /// - `BUFFER_TOO_SMALL`, when there are rows left but they cannot fit in `buffer`.
        ///   When this occurs, `buffer_len` is set to the size of the next item in the iterator.
        ///   To make progress, the caller should reallocate the buffer to at least that size and try again.
        pub fn row_iter_bsatn_advance(iter: RowIter, buffer_ptr: *mut u8, buffer_len_ptr: *mut usize) -> i16;

        /// Destroys the iterator registered under `iter`.
        ///
        /// Once `row_iter_bsatn_close` is called on `iter`, the `iter` is invalid.
        /// That is, `row_iter_bsatn_close(iter)` the second time will yield `NO_SUCH_ITER`.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NO_SUCH_ITER`, when `iter` is not a valid iterator.
        pub fn row_iter_bsatn_close(iter: RowIter) -> u16;

        /// Inserts a row into the table identified by `table_id`,
        /// where the row is read from the byte string `row = row_ptr[..row_len]` in WASM memory
        /// where `row_len = row_len_ptr[..size_of::<usize>()]` stores the capacity of `row`.
        ///
        /// The byte string `row` must be a BSATN-encoded `ProductValue`
        /// typed at the table's `ProductType` row-schema.
        ///
        /// To handle auto-incrementing columns,
        /// when the call is successful,
        /// the `row` is written back to with the generated sequence values.
        /// These values are written as a BSATN-encoded `pv: ProductValue`.
        /// Each `v: AlgebraicValue` in `pv` is typed at the sequence's column type.
        /// The `v`s in `pv` are ordered by the order of the columns, in the schema of the table.
        /// When the table has no sequences,
        /// this implies that the `pv`, and thus `row`, will be empty.
        /// The `row_len` is set to the length of `bsatn(pv)`.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `row_len_ptr` is NULL or `row_len` is not in bounds of WASM memory.
        /// - `row_ptr` is NULL or `row` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table.
        /// - `BSATN_DECODE_ERROR`, when `row` cannot be decoded to a `ProductValue`.
        ///   typed at the `ProductType` the table's schema specifies.
        /// - `UNIQUE_ALREADY_EXISTS`, when inserting `row` would violate a unique constraint.
        /// - `SCHEDULE_AT_DELAY_TOO_LONG`, when the delay specified in the row was too long.
        pub fn datastore_insert_bsatn(table_id: TableId, row_ptr: *mut u8, row_len_ptr: *mut usize) -> u16;

        /// Updates a row in the table identified by `table_id` to `row`
        /// where the row is read from the byte string `row = row_ptr[..row_len]` in WASM memory
        /// where `row_len = row_len_ptr[..size_of::<usize>()]` stores the capacity of `row`.
        ///
        /// The byte string `row` must be a BSATN-encoded `ProductValue`
        /// typed at the table's `ProductType` row-schema.
        ///
        /// The row to update is found by projecting `row`
        /// to the type of the *unique* index identified by `index_id`.
        /// If no row is found, the error `NO_SUCH_ROW` is returned.
        ///
        /// To handle auto-incrementing columns,
        /// when the call is successful,
        /// the `row` is written back to with the generated sequence values.
        /// These values are written as a BSATN-encoded `pv: ProductValue`.
        /// Each `v: AlgebraicValue` in `pv` is typed at the sequence's column type.
        /// The `v`s in `pv` are ordered by the order of the columns, in the schema of the table.
        /// When the table has no sequences,
        /// this implies that the `pv`, and thus `row`, will be empty.
        /// The `row_len` is set to the length of `bsatn(pv)`.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `row_len_ptr` is NULL or `row_len` is not in bounds of WASM memory.
        /// - `row_ptr` is NULL or `row` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table.
        /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index.
        /// - `INDEX_NOT_UNIQUE`, when the index was not unique.
        /// - `NO_SUCH_ROW`, when the row was not found in the unique index.
        /// - `BSATN_DECODE_ERROR`, when `row` cannot be decoded to a `ProductValue`
        ///   typed at the `ProductType` the table's schema specifies
        ///   or when it cannot be projected to the index identified by `index_id`.
        /// - `UNIQUE_ALREADY_EXISTS`, when inserting `row` would violate a unique constraint.
        /// - `SCHEDULE_AT_DELAY_TOO_LONG`, when the delay specified in the row was too long.
        pub fn datastore_update_bsatn(
            table_id: TableId,
            index_id: IndexId,
            row_ptr: *mut u8,
            row_len_ptr: *mut usize,
        ) -> u16;

        /// Schedules a reducer to be called asynchronously, nonatomically,
        /// and immediately on a best effort basis.
        ///
        /// The reducer is named as the valid UTF-8 slice `(name, name_len)`,
        /// and is passed the slice `(args, args_len)` as its argument.
        ///
        /// Traps if
        /// - `name` does not point to valid UTF-8
        /// - `name + name_len` or `args + args_len` overflow a 64-bit integer
        #[cfg(feature = "unstable")]
        pub fn volatile_nonatomic_schedule_immediate(
            name: *const u8,
            name_len: usize,
            args: *const u8,
            args_len: usize,
        );

        /// Writes up to `buffer_len` bytes from `buffer = buffer_ptr[..buffer_len]`,
        /// to the `sink`, registered in the host environment.
        ///
        /// The `buffer_len = buffer_len_ptr[..size_of::<usize>()]` stores the capacity of `buffer`.
        /// On success (`0` is returned),
        /// `buffer_len` is set to the number of bytes written to `sink`.
        ///
        /// # Traps
        ///
        /// - `buffer_len_ptr` is NULL or `buffer_len` is not in bounds of WASM memory.
        /// - `buffer_ptr` is NULL or `buffer` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NO_SUCH_BYTES`, when `sink` is not a valid bytes sink.
        /// - `NO_SPACE`, when there is no room for more bytes in `sink`.
        pub fn bytes_sink_write(sink: BytesSink, buffer_ptr: *const u8, buffer_len_ptr: *mut usize) -> u16;

        /// Reads bytes from `source`, registered in the host environment,
        /// and stores them in the memory pointed to by `buffer = buffer_ptr[..buffer_len]`.
        ///
        /// The `buffer_len = buffer_len_ptr[..size_of::<usize>()]` stores the capacity of `buffer`.
        /// On success (`0` or `-1` is returned),
        /// `buffer_len` is set to the number of bytes written to `buffer`.
        /// When `-1` is returned, the resource has been exhausted
        /// and there are no more bytes to read,
        /// leading to the resource being immediately destroyed.
        /// Note that the host is free to reuse allocations in a pool,
        /// destroying the handle logically does not entail that memory is necessarily reclaimed.
        ///
        /// # Traps
        ///
        /// Traps if:
        ///
        /// - `buffer_len_ptr` is NULL or `buffer_len` is not in bounds of WASM memory.
        /// - `buffer_ptr` is NULL or `buffer` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NO_SUCH_BYTES`, when `source` is not a valid bytes source.
        ///
        /// # Example
        ///
        /// The typical use case for this ABI is in `__call_reducer__`,
        /// to read and deserialize the `args`.
        /// An example definition, dealing with `args` might be:
        /// ```rust,ignore
        /// /// #[no_mangle]
        /// extern "C" fn __call_reducer__(..., args: BytesSource, ...) -> i16 {
        ///     // ...
        ///
        ///     let mut buf = Vec::<u8>::with_capacity(1024);
        ///     loop {
        ///         // Write into the spare capacity of the buffer.
        ///         let buf_ptr = buf.spare_capacity_mut();
        ///         let spare_len = buf_ptr.len();
        ///         let mut buf_len = buf_ptr.len();
        ///         let buf_ptr = buf_ptr.as_mut_ptr().cast();
        ///         let ret = unsafe { bytes_source_read(args, buf_ptr, &mut buf_len) };
        ///         // SAFETY: `bytes_source_read` just appended `spare_len` bytes to `buf`.
        ///         unsafe { buf.set_len(buf.len() + spare_len) };
        ///         match ret {
        ///             // Host side source exhausted, we're done.
        ///             -1 => break,
        ///             // Wrote the entire spare capacity.
        ///             // Need to reserve more space in the buffer.
        ///             0 if spare_len == buf_len => buf.reserve(1024),
        ///             // Host didn't write as much as possible.
        ///             // Try to read some more.
        ///             // The host will likely not trigger this branch,
        ///             // but a module should be prepared for it.
        ///             0 => {}
        ///             _ => unreachable!(),
        ///         }
        ///     }
        ///
        ///     // ...
        /// }
        /// ```
        pub fn bytes_source_read(source: BytesSource, buffer_ptr: *mut u8, buffer_len_ptr: *mut usize) -> i16;

        /// Logs at `level` a `message` message occurring in `filename:line_number`
        /// with `target` being the module path at the `log!` invocation site.
        ///
        /// These various pointers are interpreted lossily as UTF-8 strings with a corresponding `_len`.
        ///
        /// The `target` and `filename` pointers are ignored by passing `NULL`.
        /// The line number is ignored if `line_number == u32::MAX`.
        ///
        /// No message is logged if
        /// - `target != NULL && target + target_len > u64::MAX`
        /// - `filename != NULL && filename + filename_len > u64::MAX`
        /// - `message + message_len > u64::MAX`
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `target` is not NULL and `target_ptr[..target_len]` is not in bounds of WASM memory.
        /// - `filename` is not NULL and `filename_ptr[..filename_len]` is not in bounds of WASM memory.
        /// - `message` is not NULL and `message_ptr[..message_len]` is not in bounds of WASM memory.
        ///
        /// [target]: https://docs.rs/log/latest/log/struct.Record.html#method.target
        pub fn console_log(
            level: u8,
            target_ptr: *const u8,
            target_len: usize,
            filename_ptr: *const u8,
            filename_len: usize,
            line_number: u32,
            message_ptr: *const u8,
            message_len: usize,
        );

        /// Begins a timing span with `name = name_ptr[..name_len]`.
        ///
        /// When the returned `ConsoleTimerId` is passed to [`console_timer_end`],
        /// the duration between the calls will be printed to the module's logs.
        ///
        /// The `name` is interpreted lossily as UTF-8.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `name_ptr` is NULL or `name` is not in bounds of WASM memory.
        pub fn console_timer_start(name_ptr: *const u8, name_len: usize) -> u32;

        /// End a timing span.
        ///
        /// The `timer_id` must be the result of a call to `console_timer_start`.
        /// The duration between the two calls will be computed and printed to the module's logs.
        /// Once `console_timer_end` is called on `id: ConsoleTimerId`, the `id` is invalid.
        /// That is, `console_timer_end(id)` the second time will yield `NO_SUCH_CONSOLE_TIMER`.
        ///
        /// Note that the host is free to reuse allocations in a pool,
        /// destroying the handle logically does not entail that memory is necessarily reclaimed.
        ///
        /// # Errors
        ///
        /// Returns an error:
        /// - `NO_SUCH_CONSOLE_TIMER`, when `timer_id` does not exist.
        pub fn console_timer_end(timer_id: u32) -> u16;

        /// Writes the identity of the module into `out = out_ptr[..32]`.
        ///
        /// # Traps
        ///
        /// Traps if:
        ///
        /// - `out_ptr` is NULL or `out` is not in bounds of WASM memory.
        pub fn identity(out_ptr: *mut u8);

    }

    // See comment on previous `extern "C"` block re: ABI version.
    #[link(wasm_import_module = "spacetime_10.1")]
    extern "C" {
        /// Read the remaining length of a [`BytesSource`] and write it to `out`.
        ///
        /// Note that the host automatically frees byte sources which are exhausted.
        /// Such sources are invalid, and this method will return an error when passed one.
        /// Callers of [`bytes_source_read`] should check for a return of -1
        /// before invoking this function on the same `source`.
        ///
        /// Also note that the special [`BytesSource::INVALID`] (zero) is always invalid.
        /// Callers should check for that value before invoking this function.
        ///
        /// # Traps
        ///
        /// Traps if:
        ///
        /// - `out` is NULL or `out` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NO_SUCH_BYTES`, when `source` is not a valid bytes source.
        ///
        /// If this function returns an error, `out` is not written.
        pub fn bytes_source_remaining_length(source: BytesSource, out: *mut u32) -> i16;
    }

    // See comment on previous `extern "C"` block re: ABI version.
    #[link(wasm_import_module = "spacetime_10.2")]
    extern "C" {
        /// Finds the JWT payload associated with `connection_id`.
        /// A `[ByteSourceId]` for the payload will be written to `target_ptr`.
        /// If nothing is found for the connection, `[ByteSourceId::INVALID]` (zero) is written to `target_ptr`.
        ///
        /// This must be called inside a transaction (because it reads from a system table).
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
        ///
        /// # Traps
        ///
        /// Traps if:
        ///
        /// - `connection_id` does not point to a valid little-endian `ConnectionId`.
        /// - `target_ptr` is NULL or `target_ptr[..size_of::<u32>()]` is not in bounds of WASM memory.
        ///  - The `ByteSourceId` to be written to `target_ptr` would overflow [`u32::MAX`].
        pub fn get_jwt(connection_id_ptr: *const u8, bytes_source_id: *mut BytesSource) -> u16;
    }

    #[cfg(feature = "unstable")]
    #[link(wasm_import_module = "spacetime_10.3")]
    extern "C" {
        /// Suspends execution of this WASM instance until approximately `wake_at_micros_since_unix_epoch`.
        ///
        /// Returns immediately if `wake_at_micros_since_unix_epoch` is in the past.
        ///
        /// Upon resuming, returns the current timestamp as microseconds since the Unix epoch.
        ///
        /// Not particularly useful, except for testing SpacetimeDB internals related to suspending procedure execution.
        /// # Traps
        ///
        /// Traps if:
        ///
        /// - The calling WASM instance is holding open a transaction.
        /// - The calling WASM instance is not executing a procedure.
        // TODO(procedure-sleep-until): remove this
        pub fn procedure_sleep_until(wake_at_micros_since_unix_epoch: i64) -> i64;

        /// Starts a mutable transaction,
        /// suspending execution of this WASM instance until
        /// a mutable transaction lock is aquired.
        ///
        /// Upon resuming, returns `0` on success,
        /// enabling further calls that require a pending transaction,
        /// or an error code otherwise.
        ///
        /// # Traps
        ///
        /// Traps if:
        /// - `out` is NULL or `out[..size_of::<i64>()]` is not in bounds of WASM memory.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `WOULD_BLOCK_TRANSACTION`, if there's already an ongoing transaction.
        pub fn procedure_start_mut_tx(out: *mut i64) -> u16;

        /// Commits a mutable transaction,
        /// suspending execution of this WASM instance until
        /// the transaction has been committed
        /// and subscription queries have been run and broadcast.
        ///
        /// Upon resuming, returns `0` on success, or an error code otherwise.
        ///
        /// # Traps
        ///
        /// This function does not trap.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `TRANSACTION_NOT_ANONYMOUS`,
        ///   if the transaction was not started in [`procedure_start_mut_tx`].
        ///   This can happen if this syscall is erroneously called by a reducer.
        ///   The code `NOT_IN_TRANSACTION` does not happen,
        ///   as it is subsumed by `TRANSACTION_NOT_ANONYMOUS`.
        /// - `TRANSACTION_IS_READ_ONLY`, if the pending transaction is read-only.
        ///   This currently does not happen as anonymous read transactions
        ///   are not exposed to modules.
        pub fn procedure_commit_mut_tx() -> u16;

        /// Aborts a mutable transaction,
        /// suspending execution of this WASM instance until
        /// the transaction has been rolled back.
        ///
        /// Upon resuming, returns `0` on success, or an error code otherwise.
        ///
        /// # Traps
        ///
        /// This function does not trap.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `TRANSACTION_NOT_ANONYMOUS`,
        ///   if the transaction was not started in [`procedure_start_mut_tx`].
        ///   This can happen if this syscall is erroneously called by a reducer.
        ///   The code `NOT_IN_TRANSACTION` does not happen,
        ///   as it is subsumed by `TRANSACTION_NOT_ANONYMOUS`.
        /// - `TRANSACTION_IS_READ_ONLY`, if the pending transaction is read-only.
        ///   This currently does not happen as anonymous read transactions
        ///   are not exposed to modules.
        pub fn procedure_abort_mut_tx() -> u16;

        /// Perform an HTTP request as specified by the buffer `request_ptr[..request_len]`,
        /// suspending execution until the request is complete,
        /// then return its response via a [`BytesSource`] written to `out`.
        ///
        /// `request_ptr[..request_len]` should store a BSATN-serialized `spacetimedb_lib::http::Request` object
        /// containing the details of the request to be performed.
        ///
        /// If the request is successful, a [`BytesSource`] is written to `out`
        /// containing a BSATN-encoded `spacetimedb_lib::http::Response` object.
        /// "Successful" in this context includes any connection which results in any HTTP status code,
        /// regardless of the specified meaning of that code.
        ///
        /// # Errors
        ///
        /// Returns an error:
        ///
        /// - `WOULD_BLOCK_TRANSACTION` if there is currently a transaction open.
        ///   In this case, `out` is not written.
        /// - `BSATN_DECODE_ERROR` if `request_ptr[..request_len]` does not contain
        ///   a valid BSATN-serialized `spacetimedb_lib::http::Request` object.
        ///   In this case, `out` is not written.
        /// - `HTTP_ERROR` if an error occurs while executing the HTTP request.
        ///   In this case, a [`BytesSource`] is written to `out`
        ///   containing a BSATN-encoded `spacetimedb_lib::http::Error` object.
        ///
        /// # Traps
        ///
        /// Traps if:
        ///
        /// - `request_ptr` is NULL or `request_ptr[..request_len]` is not in bounds of WASM memory.
        /// - `out` is NULL or `out[..size_of::<RowIter>()]` is not in bounds of WASM memory.
        /// - `request_ptr[..request_len]` does not contain a valid BSATN-serialized `spacetimedb_lib::http::Request` object.
        #[cfg(feature = "unstable")]
        pub fn procedure_http_request(
            request_ptr: *const u8,
            request_len: u32,
            body_ptr: *const u8,
            body_len: u32,
            out: *mut [BytesSource; 2],
        ) -> u16;
    }

    /// What strategy does the database index use?
    ///
    /// See also: <https://www.postgresql.org/docs/current/sql-createindex.html>
    #[repr(u8)]
    #[non_exhaustive]
    pub enum IndexType {
        /// Indexing works by putting the index key into a b-tree.
        BTree = 0,
        /// Indexing works by hashing the index key.
        Hash = 1,
    }

    /// The error log level. See [`console_log`].
    pub const LOG_LEVEL_ERROR: u8 = 0;
    /// The warn log level. See [`console_log`].
    pub const LOG_LEVEL_WARN: u8 = 1;
    /// The info log level. See [`console_log`].
    pub const LOG_LEVEL_INFO: u8 = 2;
    /// The debug log level. See [`console_log`].
    pub const LOG_LEVEL_DEBUG: u8 = 3;
    /// The trace log level. See [`console_log`].
    pub const LOG_LEVEL_TRACE: u8 = 4;
    /// The panic log level. See [`console_log`].
    ///
    /// A panic level is emitted just before a fatal error causes the WASM module to trap.
    pub const LOG_LEVEL_PANIC: u8 = 101;

    /// A handle into a buffer of bytes in the host environment that can be read from.
    ///
    /// Used for transporting bytes from host to WASM linear memory.
    #[derive(PartialEq, Eq, Copy, Clone)]
    #[repr(transparent)]
    pub struct BytesSource(u32);

    impl BytesSource {
        /// An invalid handle, used e.g., when the reducer arguments were empty.
        pub const INVALID: Self = Self(0);
    }

    /// A handle into a buffer of bytes in the host environment that can be written to.
    ///
    /// Used for transporting bytes from WASM linear memory to host.
    #[derive(PartialEq, Eq, Copy, Clone)]
    #[repr(transparent)]
    pub struct BytesSink(u32);

    /// Represents table iterators.
    #[derive(PartialEq, Eq, Copy, Clone)]
    #[repr(transparent)]
    pub struct RowIter(u32);

    impl RowIter {
        /// An invalid handle, used e.g., when the iterator has been exhausted.
        pub const INVALID: Self = Self(0);
    }

    #[cfg(any())]
    mod module_exports {
        type Encoded<T> = Buffer;
        type Identity = Encoded<[u8; 32]>;
        /// Microseconds since the unix epoch
        type Timestamp = u64;
        /// Buffer::INVALID => Ok(()); else errmsg => Err(errmsg)
        type Result = Buffer;
        extern "C" {
            /// All functions prefixed with `__preinit__` are run first in alphabetical order.
            /// For those it's recommended to use /etc/xxxx.d conventions of like `__preinit__20_do_thing`:
            /// <https://man7.org/linux/man-pages/man5/sysctl.d.5.html#CONFIGURATION_DIRECTORIES_AND_PRECEDENCE>
            fn __preinit__XX_XXXX();
            /// Optional. Run after `__preinit__`; can return an error. Intended for dynamic languages; this
            /// would be where you would initialize the interepreter and load the user module into it.
            fn __setup__() -> Result;
            /// Required. Runs after `__setup__`; returns all the exports for the module.
            fn __describe_module__() -> Encoded<ModuleDef>;
            /// Required. id is an index into the `ModuleDef.reducers` returned from `__describe_module__`.
            /// args is a bsatn-encoded product value defined by the schema at `reducers[id]`.
            fn __call_reducer__(
                id: usize,
                sender_0: u64,
                sender_1: u64,
                sender_2: u64,
                sender_3: u64,
                conn_id_0: u64,
                conn_id_1: u64,
                timestamp: u64,
                args: Buffer,
            ) -> Result;
            /// Currently unused?
            fn __migrate_database__XXXX(sender: Identity, timestamp: Timestamp, something: Buffer) -> Result;
        }
    }
}

/// Error values used in the safe bindings API.
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct Errno(NonZeroU16);

// once Error gets exposed from core this crate can be no_std again
impl std::error::Error for Errno {}

pub type Result<T, E = Errno> = core::result::Result<T, E>;

macro_rules! def_errno {
    ($($err_name:ident($errno:literal, $errmsg:literal),)*) => {
        impl Errno {
            $(#[doc = $errmsg] pub const $err_name: Errno = Errno(errno::$err_name);)*
        }
    };
}
errnos!(def_errno);

impl Errno {
    /// Returns a description of the errno value, if any.
    pub const fn message(self) -> Option<&'static str> {
        errno::strerror(self.0)
    }

    /// Converts the given `code` to an error number in `Errno`'s representation.
    #[inline]
    pub const fn from_code(code: u16) -> Option<Self> {
        match NonZeroU16::new(code) {
            Some(code) => Some(Errno(code)),
            None => None,
        }
    }

    /// Converts this `errno` into a primitive error code.
    #[inline]
    pub const fn code(self) -> u16 {
        self.0.get()
    }
}

impl fmt::Debug for Errno {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut fmt = f.debug_struct("Errno");
        fmt.field("code", &self.code());
        if let Some(msg) = self.message() {
            fmt.field("message", &msg);
        }
        fmt.finish()
    }
}

impl fmt::Display for Errno {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let message = self.message().unwrap_or("Unknown error");
        write!(f, "{message} (error {})", self.code())
    }
}

/// Convert the status value `x` into a result.
/// When `x = 0`, we have a success status.
fn cvt(x: u16) -> Result<()> {
    match Errno::from_code(x) {
        None => Ok(()),
        Some(err) => Err(err),
    }
}

/// Runs the given function `f` provided with an uninitialized `out` pointer.
///
/// Assuming the call to `f` succeeds (`Ok(_)`), the `out` pointer's value is returned.
///
/// # Safety
///
/// This function is safe to call, if and only if,
/// - The function `f` writes a safe and valid `T` to the `out` pointer.
///   It's not required to write to `out` when `f(out)` returns an error code.
/// - The function `f` never reads a safe and valid `T` from the `out` pointer
///   before writing a safe and valid `T` to it.
#[inline]
unsafe fn call<T: Copy>(f: impl FnOnce(*mut T) -> u16) -> Result<T> {
    let mut out = MaybeUninit::uninit();
    let f_code = f(out.as_mut_ptr());
    cvt(f_code)?;
    Ok(out.assume_init())
}

/// Runs the given function `f`.
///
/// Assuming the call to `f` returns 0, `Ok(())` is returned,
/// and otherwise `Err(err)` is returned.
#[inline]
#[cfg(feature = "unstable")]
fn call_no_ret(f: impl FnOnce() -> u16) -> Result<()> {
    let f_code = f();
    cvt(f_code)?;
    Ok(())
}

/// Queries the `table_id` associated with the given (table) `name`.
///
/// The table id is returned.
///
/// # Errors
///
/// Returns an error:
///
/// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
/// - `NO_SUCH_TABLE`, when `name` is not the name of a table.
#[inline]
pub fn table_id_from_name(name: &str) -> Result<TableId> {
    unsafe { call(|out| raw::table_id_from_name(name.as_ptr(), name.len(), out)) }
}

/// Queries the `index_id` associated with the given (index) `name`.
///
/// The index id is returned.
///
/// # Errors
///
/// Returns an error:
///
/// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
/// - `NO_SUCH_INDEX`, when `name` is not the name of an index.
#[inline]
pub fn index_id_from_name(name: &str) -> Result<IndexId> {
    unsafe { call(|out| raw::index_id_from_name(name.as_ptr(), name.len(), out)) }
}

/// Returns the number of rows currently in table identified by `table_id`.
///
/// # Errors
///
/// Returns an error:
///
/// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
/// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table.
#[inline]
pub fn datastore_table_row_count(table_id: TableId) -> Result<u64> {
    unsafe { call(|out| raw::datastore_table_row_count(table_id, out)) }
}

/// Inserts a row into the table identified by `table_id`,
/// where the row is a BSATN-encoded `ProductValue`
/// matching the table's `ProductType` row-schema.
///
/// The `row` is `&mut` due to auto-incrementing columns.
/// So `row` is written to with the inserted row re-encoded.
///
/// Returns an error if
/// - a table with the provided `table_id` doesn't exist
/// - there were unique constraint violations
/// - `row` doesn't decode from BSATN to a `ProductValue`
///   according to the `ProductType` that the table's schema specifies.
#[inline]
pub fn datastore_insert_bsatn(table_id: TableId, row: &mut [u8]) -> Result<&[u8]> {
    let row_ptr = row.as_mut_ptr();
    let row_len = &mut row.len();
    cvt(unsafe { raw::datastore_insert_bsatn(table_id, row_ptr, row_len) }).map(|()| &row[..*row_len])
}

/// Updates a row into the table identified by `table_id`,
/// where the row is a BSATN-encoded `ProductValue`
/// matching the table's `ProductType` row-schema.
///
/// The row to update is found by projecting `row`
/// to the type of the *unique* index identified by `index_id`.
/// If no row is found, `row` is inserted.
///
/// The `row` is `&mut` due to auto-incrementing columns.
/// So `row` is written to with the updated row re-encoded.
///
/// Returns an error if
/// - a table with the provided `table_id` doesn't exist
/// - an index with the provided `index_id` doesn't exist or if the index was not unique.
/// - there were unique constraint violations
/// - `row` doesn't decode from BSATN to a `ProductValue`
///   according to the `ProductType` that the table's schema specifies
///   or if `row` cannot project to the index's type.
/// - the row was not found
#[inline]
pub fn datastore_update_bsatn(table_id: TableId, index_id: IndexId, row: &mut [u8]) -> Result<&[u8]> {
    let row_ptr = row.as_mut_ptr();
    let row_len = &mut row.len();
    cvt(unsafe { raw::datastore_update_bsatn(table_id, index_id, row_ptr, row_len) }).map(|()| &row[..*row_len])
}

/// Deletes those rows, in the table identified by `table_id`,
/// that match any row in the byte string `relation`.
///
/// Matching is defined by first BSATN-decoding
/// the byte string pointed to at by `relation` to a `Vec<ProductValue>`
/// according to the row schema of the table
/// and then using `Ord for AlgebraicValue`.
/// A match happens when `Ordering::Equal` is returned from `fn cmp`.
/// This occurs exactly when the row's BSATN-encoding is equal to the encoding of the `ProductValue`.
///
/// The number of rows deleted is returned.
///
/// # Errors
///
/// Returns an error:
///
/// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
/// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table.
/// - `BSATN_DECODE_ERROR`, when `rel` cannot be decoded to `Vec<ProductValue>`
///   where each `ProductValue` is typed at the `ProductType` the table's schema specifies.
#[inline]
pub fn datastore_delete_all_by_eq_bsatn(table_id: TableId, relation: &[u8]) -> Result<u32> {
    unsafe { call(|out| raw::datastore_delete_all_by_eq_bsatn(table_id, relation.as_ptr(), relation.len(), out)) }
}

/// Starts iteration on each row, as BSATN-encoded, of a table identified by `table_id`.
/// Returns iterator handle is written to the `out` pointer.
/// This handle can be advanced by [`RowIter::read`].
///
/// # Errors
///
/// Returns an error:
///
/// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
/// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table.
pub fn datastore_table_scan_bsatn(table_id: TableId) -> Result<RowIter> {
    let raw = unsafe { call(|out| raw::datastore_table_scan_bsatn(table_id, out))? };
    Ok(RowIter { raw })
}

/// Finds all rows in the index identified by `index_id`,
/// according to the `prefix`, `rstart`, and `rend`.
///
/// The index itself has a schema/type.
/// The `prefix` is decoded to the initial `prefix_elems` `AlgebraicType`s
/// whereas `rstart` and `rend` are decoded to the `prefix_elems + 1` `AlgebraicType`
/// where the `AlgebraicValue`s are wrapped in `Bound`.
/// That is, `rstart, rend` are BSATN-encoded `Bound<AlgebraicValue>`s.
///
/// Matching is then defined by equating `prefix`
/// to the initial `prefix_elems` columns of the index
/// and then imposing `rstart` as the starting bound
/// and `rend` as the ending bound on the `prefix_elems + 1` column of the index.
/// Remaining columns of the index are then unbounded.
/// Note that the `prefix` in this case can be empty (`prefix_elems = 0`),
/// in which case this becomes a ranged index scan on a single-col index
/// or even a full table scan if `rstart` and `rend` are both unbounded.
///
/// The relevant table for the index is found implicitly via the `index_id`,
/// which is unique for the module.
///
/// On success, the iterator handle is written to the `out` pointer.
/// This handle can be advanced by [`RowIter::read`].
///
/// # Non-obvious queries
///
/// For an index on columns `[a, b, c]`:
///
/// - `a = x, b = y` is encoded as a prefix `[x, y]`
///   and a range `Range::Unbounded`,
///   or as a  prefix `[x]` and a range `rstart = rend = Range::Inclusive(y)`.
/// - `a = x, b = y, c = z` is encoded as a prefix `[x, y]`
///   and a  range `rstart = rend = Range::Inclusive(z)`.
/// - A sorted full scan is encoded as an empty prefix
///   and a range `Range::Unbounded`.
///
/// # Errors
///
/// Returns an error:
///
/// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
/// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index.
/// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index.
/// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to
///   a `prefix_elems` number of `AlgebraicValue`
///   typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type.
///   Or when `rstart` or `rend` cannot be decoded to an `Bound<AlgebraicValue>`
///   where the inner `AlgebraicValue`s are
///   typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type.
pub fn datastore_index_scan_range_bsatn(
    index_id: IndexId,
    prefix: &[u8],
    prefix_elems: ColId,
    rstart: &[u8],
    rend: &[u8],
) -> Result<RowIter> {
    let raw = unsafe {
        call(|out| {
            raw::datastore_index_scan_range_bsatn(
                index_id,
                prefix.as_ptr(),
                prefix.len(),
                prefix_elems,
                rstart.as_ptr(),
                rstart.len(),
                rend.as_ptr(),
                rend.len(),
                out,
            )
        })?
    };
    Ok(RowIter { raw })
}

/// Deletes all rows found in the index identified by `index_id`,
/// according to the `prefix`, `rstart`, and `rend`.
///
/// This syscall will delete all the rows found by
/// [`datastore_index_scan_range_bsatn`] with the same arguments passed,
/// including `prefix_elems`.
/// See `datastore_index_scan_range_bsatn` for details.
///
/// The number of rows deleted is returned on success.
///
/// # Errors
///
/// Returns an error:
///
/// - `NOT_IN_TRANSACTION`, when called outside of a transaction.
/// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index.
/// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index.
/// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to
///   a `prefix_elems` number of `AlgebraicValue`
///   typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type.
///   Or when `rstart` or `rend` cannot be decoded to an `Bound<AlgebraicValue>`
///   where the inner `AlgebraicValue`s are
///   typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type.
pub fn datastore_delete_by_index_scan_range_bsatn(
    index_id: IndexId,
    prefix: &[u8],
    prefix_elems: ColId,
    rstart: &[u8],
    rend: &[u8],
) -> Result<u32> {
    unsafe {
        call(|out| {
            raw::datastore_delete_by_index_scan_range_bsatn(
                index_id,
                prefix.as_ptr(),
                prefix.len(),
                prefix_elems,
                rstart.as_ptr(),
                rstart.len(),
                rend.as_ptr(),
                rend.len(),
                out,
            )
        })
    }
}

/// A log level that can be used in `console_log`.
/// The variants are convertible into a raw `u8` log level.
#[repr(u8)]
pub enum LogLevel {
    /// The error log level. See [`console_log`].
    Error = raw::LOG_LEVEL_ERROR,
    /// The warn log level. See [`console_log`].
    Warn = raw::LOG_LEVEL_WARN,
    /// The info log level. See [`console_log`].
    Info = raw::LOG_LEVEL_INFO,
    /// The debug log level. See [`console_log`].
    Debug = raw::LOG_LEVEL_DEBUG,
    /// The trace log level. See [`console_log`].
    Trace = raw::LOG_LEVEL_TRACE,
    /// The panic log level. See [`console_log`].
    ///
    /// A panic level is emitted just before a fatal error causes the WASM module to trap.
    Panic = raw::LOG_LEVEL_PANIC,
}

/// Log at `level` a `text` message occurring in `filename:line_number`
/// with [`target`] being the module path at the `log!` invocation site.
///
/// [`target`]: https://docs.rs/log/latest/log/struct.Record.html#method.target
#[inline]
pub fn console_log(
    level: LogLevel,
    target: Option<&str>,
    filename: Option<&str>,
    line_number: Option<u32>,
    text: &str,
) {
    let opt_ptr = |b: Option<&str>| b.map_or(ptr::null(), |b| b.as_ptr());
    let opt_len = |b: Option<&str>| b.map_or(0, |b| b.len());
    unsafe {
        raw::console_log(
            level as u8,
            opt_ptr(target),
            opt_len(target),
            opt_ptr(filename),
            opt_len(filename),
            line_number.unwrap_or(u32::MAX),
            text.as_ptr(),
            text.len(),
        )
    }
}

/// Schedule a reducer to be called asynchronously, nonatomically, and immediately
/// on a best-effort basis.
///
/// The reducer is assigned `name` and is provided `args` as its argument.
#[cfg(feature = "unstable")]
#[inline]
pub fn volatile_nonatomic_schedule_immediate(name: &str, args: &[u8]) {
    unsafe { raw::volatile_nonatomic_schedule_immediate(name.as_ptr(), name.len(), args.as_ptr(), args.len()) }
}

/// Read the current module's identity, as a little-endian byte array.
///
/// Doesn't return a proper typed `Identity` because this crate doesn't depend on `spacetimedb_lib`.
#[inline]
pub fn identity() -> [u8; 32] {
    let mut buf = [0u8; 32];
    unsafe {
        raw::identity(buf.as_mut_ptr());
    }
    buf
}

/// Finds the JWT payload associated with `connection_id`.
/// If nothing is found for the connection, this returns None.
/// If a payload is found, this will return a valid [`raw::BytesSource`].
///
/// This must be called inside a transaction (because it reads from a system table).
///
/// # Errors
///
/// This panics on any error. You can see details about errors in [`raw::get_jwt`].
#[inline]
pub fn get_jwt(connection_id: [u8; 16]) -> Option<raw::BytesSource> {
    let source = unsafe {
        call(|out| raw::get_jwt(connection_id.as_ptr(), out))
            .unwrap_or_else(|errno: Errno| panic!("Error getting jwt: {errno}"))
    };

    if source == raw::BytesSource::INVALID {
        None // No JWT found.
    } else {
        Some(source)
    }
}

pub struct RowIter {
    raw: raw::RowIter,
}

impl RowIter {
    /// Read some number of BSATN-encoded rows into the provided buffer.
    ///
    /// Returns the number of new bytes added to the end of the buffer.
    /// When the iterator has been exhausted,
    /// `self.is_exhausted()` will return `true`.
    pub fn read(&mut self, buf: &mut Vec<u8>) -> usize {
        loop {
            let buf_ptr = buf.spare_capacity_mut();
            let mut buf_len = buf_ptr.len();
            let ret = unsafe { raw::row_iter_bsatn_advance(self.raw, buf_ptr.as_mut_ptr().cast(), &mut buf_len) };
            if let -1 | 0 = ret {
                // SAFETY: `_row_iter_bsatn_advance` just wrote `buf_len` bytes into the end of `buf`.
                unsafe { buf.set_len(buf.len() + buf_len) };
            }

            const TOO_SMALL: i16 = errno::BUFFER_TOO_SMALL.get() as i16;
            match ret {
                -1 => {
                    self.raw = raw::RowIter::INVALID;
                    return buf_len;
                }
                0 => return buf_len,
                TOO_SMALL => buf.reserve(buf_len),
                e => panic!("unexpected error from `_row_iter_bsatn_advance`: {e}"),
            }
        }
    }

    /// Returns whether the iterator is exhausted or not.
    pub fn is_exhausted(&self) -> bool {
        self.raw == raw::RowIter::INVALID
    }
}

impl Drop for RowIter {
    fn drop(&mut self) {
        // Avoid this syscall when `_row_iter_bsatn_advance` above
        // notifies us that the iterator is exhausted.
        if self.is_exhausted() {
            return;
        }
        unsafe {
            raw::row_iter_bsatn_close(self.raw);
        }
    }
}

#[cfg(feature = "unstable")]
pub mod procedure {
    //! Side-effecting or asynchronous operations which only procedures are allowed to perform.

    use super::{call, call_no_ret, raw, Result};

    #[inline]
    pub fn sleep_until(wake_at_timestamp: i64) -> i64 {
        // Safety: Just calling an `extern "C"` function.
        // Nothing weird happening here.
        unsafe { raw::procedure_sleep_until(wake_at_timestamp) }
    }

    /// Starts a mutable transaction,
    /// suspending execution of this WASM instance until
    /// a mutable transaction lock is aquired.
    ///
    /// Upon resuming, returns `Ok(timestamp)` on success,
    /// enabling further calls that require a pending transaction,
    /// or [`Errno`] otherwise.
    ///
    /// # Errors
    ///
    /// Returns an error:
    ///
    /// - `WOULD_BLOCK_TRANSACTION`, if there's already an ongoing transaction.
    #[inline]
    pub fn procedure_start_mut_tx() -> Result<i64> {
        unsafe { call(|out| raw::procedure_start_mut_tx(out)) }
    }

    /// Commits a mutable transaction,
    /// suspending execution of this WASM instance until
    /// the transaction has been committed
    /// and subscription queries have been run and broadcast.
    ///
    /// Upon resuming, returns `Ok(()` on success, or an [`Errno`] otherwise.
    ///
    /// # Errors
    ///
    /// Returns an error:
    ///
    /// - `TRANSACTION_NOT_ANONYMOUS`,
    ///   if the transaction was not started in [`procedure_start_mut_tx`].
    ///   This can happen if this syscall is erroneously called by a reducer.
    ///   The code `NOT_IN_TRANSACTION` does not happen,
    ///   as it is subsumed by `TRANSACTION_NOT_ANONYMOUS`.
    /// - `TRANSACTION_IS_READ_ONLY`, if the pending transaction is read-only.
    ///   This currently does not happen as anonymous read transactions
    ///   are not exposed to modules.
    #[inline]
    pub fn procedure_commit_mut_tx() -> Result<()> {
        call_no_ret(|| unsafe { raw::procedure_commit_mut_tx() })
    }

    /// Aborts a mutable transaction,
    /// suspending execution of this WASM instance until
    /// the transaction has been rolled back.
    ///
    /// Upon resuming, returns `Ok(())` on success, or an [`Errno`] otherwise.
    ///
    /// # Errors
    ///
    /// Returns an error:
    ///
    /// - `TRANSACTION_NOT_ANONYMOUS`,
    ///   if the transaction was not started in [`procedure_start_mut_tx`].
    ///   This can happen if this syscall is erroneously called by a reducer.
    ///   The code `NOT_IN_TRANSACTION` does not happen,
    ///   as it is subsumed by `TRANSACTION_NOT_ANONYMOUS`.
    /// - `TRANSACTION_IS_READ_ONLY`, if the pending transaction is read-only.
    ///   This currently does not happen as anonymous read transactions
    ///   are not exposed to modules.
    #[inline]
    pub fn procedure_abort_mut_tx() -> Result<()> {
        call_no_ret(|| unsafe { raw::procedure_abort_mut_tx() })
    }

    #[inline]
    #[cfg(feature = "unstable")]
    /// Perform an HTTP request as specified by `http_request_bsatn`,
    /// suspending execution until the request is complete,
    /// then return its response or error.
    ///
    /// `http_request_bsatn` should be a BSATN-serialized `spacetimedb_lib::http::Request`.
    ///
    /// If the request completes successfully,
    /// this function returns `Ok(bytes)`, where `bytes` contains a BSATN-serialized `spacetimedb_lib::http::Response`.
    /// All HTTP response codes are treated as successful for these purposes;
    /// this method only returns an error if it is unable to produce any HTTP response whatsoever.
    /// In that case, this function returns `Err(bytes)`, where `bytes` contains a BSATN-serialized `spacetimedb_lib::http::Error`.
    pub fn http_request(
        http_request_bsatn: &[u8],
        body: &[u8],
    ) -> Result<(raw::BytesSource, raw::BytesSource), raw::BytesSource> {
        let mut out = [raw::BytesSource::INVALID; 2];

        let res = unsafe {
            super::raw::procedure_http_request(
                http_request_bsatn.as_ptr(),
                http_request_bsatn.len() as u32,
                body.as_ptr(),
                body.len() as u32,
                &mut out as *mut [raw::BytesSource; 2],
            )
        };

        match super::Errno::from_code(res) {
            // Success: `out` is a `spacetimedb_lib::http::Response`.
            None => Ok((out[0], out[1])),
            // HTTP_ERROR: `out` is a `spacetimedb_lib::http::Error`.
            Some(errno) if errno == super::Errno::HTTP_ERROR => Err(out[0]),
            Some(errno) => panic!("{errno}"),
        }
    }
}
